Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

30.Gün - Word Scramble Uygulamasını İnşa Ediyoruz

·1823 kelime·9 dk

Bugünkü yazımızda 29.günde öğrendiğimiz List, UITextChecker ve App Bundle’ı gerçek bir uygulamada kullanacağız ve oyunumuz World Scramble’ı inşa edeceğiz. İşte başlıyoruz.

Bu proje aynı zamanda GitHub’da da bulunmaktadır.

GitHub - GorkemGuray/Word-Scramble: 100 Days of SwiftUI - Project-5

Kelime Listesini Ekleme #

Bu uygulamanın kullanıcı arayüzü üç ana SwiftUI view’dan oluşacaktır;

  • Heceledikleri kelimeyi gösteren bir NavigationStack
  • Bir cevap girebildikleri TextField
  • Daha önce girdikleri tüm kelimeleri gösteren bir List

Şimdilik, kullanıcılar metin alanına her kelime girdiğinde, bu kelimeyi otomatik olarak kullanılan kelimeler listesine ekleyeceğiz. Ancak daha sonra, kelimenin daha önce kullanılmadığından, verilen kök kelimeden gerçekten üretilebileceğinden ve sadece rastgele harfler değil gerçek bir kelime olduğundan emin olmak için bazı doğrulamalar ekleyeceğiz.

Temel bilgiler ile başlayalım: daha önce kullandıkları kelimelerden oluşan bir array, diğer kelimeleri hecelemeleri için bir kök kelimeye ve bir text alanına bağlayabileceğimiz string’e ihtiyacımız var. Şimdi bu üç property’yi ContenView’a ekleyelim;

@State private var usedWords = [String]()
@State private var rootWord = ""
@State private var newWord = ""

body ’de ise mümkün olduğunca basit bir şekilde başlayacağız. Başlığı rootWord olan bir NavigationStack ardından List içerisinde birkaç Section oluşturacağız;

var body: some View {
    NavigationStack {
        List {
            Section {
                TextField("Enter your word", text: $newWord)
            }

            Section {
                ForEach(usedWords, id: \.self) { word in
                    Text(word)
                }
            }
        }
        .navigationTitle(rootWord)
    }
}

id: \.self kullanmak usedWords ’de çok sayıda aynı kopyadan olması durumunda sorunlara neden olabilir, fakat daha sonra bunun üstesinden geleceğiz.

Şimdi, text view’da bir sorun var. Metin kutusuna yazabilsek de, oradan hiç bir şey gönderemiyoruz, girdimizi kullanılan kelimeler listesine eklemenin bir yolu yok.

Bu sorunu düzeltmek için addNewWord() adında yeni bir method yazacağız;

  1. newWord ’ü küçük harfli haline çevirecek ve tüm boşlukları kaldıracak.
  2. En az 1 karakter olup olmadığını kontrol edecek aksi takdirde methoddan çıkacak
  3. Bu sözcüğü usedWords array’de 0 konumuna ekleyecek.
  4. newWord ’ü tekrar boş bir string olarak ayarlayacak.

Daha sonra kelimenin izin verilebilir olduğundan emin olmak için 2. ve 3. adımlar arasına bazı ekstra doğrulamalar ekleyeceğiz. Fakat şimdilik bu method işimizi görecek;

func addNewWord() {
    // lowercase and trim the word, to make sure we don't add duplicate words with case differences
    // büyük/küçük harf farkı olan yinelenen sözcükler eklemedğinizden emin olmak için sözcüğü küçük harfle yazın ve trim yapın
    let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)

    // exit if the remaining string is empty
    // kalan string boşsa çık
    guard answer.count > 0 else { return }

    // extra validation to come
    // fazldan doğrulama gelecek

    usedWords.insert(answer, at: 0)
    newWord = ""
}

Kullanıcı klavyeden return tuşuna bastığında addNewWord() methodunu çağırmak istiyoruz ve SwiftUI’de bunu görünüm hiyerarşimizde bir yere onSubmit() modifier’ını ekleyerek yapabiliriz. Doğrudan buton üzerinde olabilir, ancak herhangi bir metin gönderildiğinde tetikleneceği için view’da başka bir yerde de olabilir.

onSubmit() ’a parametre kabul etmeyen ve hiçbir şey döndürmeyen bir fonksiyon verilmesi gerekir, ki bu da az önce yazdığımız addNewWord() methodu ile tam olarak eşleşir. Dolayısıyla, navigationTitle() methodunun altına bu modifier’ı ekleyerek, yazdığımız addNewWord methodunu doğrudan aktarabiliriz.

.onSubmit(addNewWord)

Word Scramble First View

Metin alanına kelime girip return tuşuna bastığınızda, kelimelerin listeye eklendiğini görebilirsiniz

addNewWord() içinde usedWord.insert(answer, at:0) kullanmamızın bir nedeni var. append(answer) kullansaydık yeni kelimeler listenin sonunda görünecekti ve muhtemelen ekran dışında kalacaklardı, ancak kelimeleri dizinin başına eklediğimizde otomatik olarak listenin başında görünüyorlar, bu çok daha iyi gözüküyor.

Navigation bar’a bir başlık koymadan önce, layout’da iki küçük değişiklik yapacağız.

İlk olarak, addNewWord() methodunu çağırdığımızda, kullanıcının girdiği kelimenin baş harfi küçük olur. Bu da kullanıcının “car”, “Car” ve “CAR” ekleyemeyeceği anlamına geldiği için yararlıdır. Ancak pratikte garip görünüyor. Text field otomatik olarak kullanıcı ne yazarsa yazsın ilk harfini büyük yazıyor, bu yüzden “Car” yazdığında listede gördüğü şey “car” oluyor.

Bunu düzeltmek için, metin alanı için büyük harf kullanımını başka bir modifier ile devre dışı bırakabiliriz : textInputAutocapitalization() şimdi bunu text field a ekleyelim.

.textInputAutocapitalization(.never)

Yapacağımız ikinci şey ise, metnin yanındaki her kelimenin uzunğunu göstermek için Apple’ın SF Symbols simgelerini kullanmak olacak. SF Symbols, 0’dan 50’ye kadar daireler içinde sayılar sağlar ve hepsi “x.circle.fill” formatı kullanılarak adlandırılır.

Dolayısıyla, kelime metnimizi bir HStack içine sarabilir ve Image(systemName:) kullanarak yanına aşağıdaki gibi bir SF Symbol yerleştirebiliriz;

ForEach(usedWords, id: \.self) { word in
    HStack {
        Image(systemName: "\(word.count).circle")
        Text(word)
    }
}

Word Count

Şu anda text field’ı submit ettiğimizde metin hemen listede görünür, ancak addNewWord() içindeki insert() çağrısını şu şekilde değiştirirsek bu işlemi animasyon ile yapabiliriz.

withAnimation {
    usedWords.insert(answer, at: 0)
}

SwiftUI Uygulama Başladığında Kod Çalıştırma #

Xcode bir iOS projesi oluşturulduğunda, derlenmiş programımızı, asset kataloğumuzu ve diğer tüm asset’leri paket adı verilen tek bir dizine yerleştirir ve ardından bu pakete YouAppName.app adını verir. Bu .app uzantısı iOS ve Apple’ın diğer platformları tarafından otomatik olarak tanınır, bu nedenle macOS’te Notes.app gibi bir şeye çift tıkladığınızda pakedin içindeki programı başlatacağını bilir.

Oyunumuzda, oyunun çalışması için rastgele seçilecek olan 10.000’den fazla sekiz harfli kelimeyi içeren “start.txt” adlı bir dosya ekleyeceğiz. Bu projeyi GitHub’dan indirerek, projenizin içine atın.

Oyuncunun hecelemesini istediğimiz kelimeyi içerecek rootWord adlı bir property’yi daha önce tanımladık. Şimdi yapmamız gereken startGame() adında yeni bir metot yazmak;

  1. Paketimizdeki start.txt dosyasını bul
  2. Bir string’e yükle
  3. Bu string’i, her öğesi bir sözcük olacak şekilde string array’a böl
  4. Buradan rootWord ’e atamak üzere rastgele bir kelime seç veya array boşsa mantıklı bir varsayılan kullan

Bu dört görevin her biri bir kod satırına karşılık gelir, ancak şöyle bir durum var: ya start.txt dosyasını uygulama paketimizde bulamazsak ya da bulsak da yükleyemezsek? Bu durumda ciddi bir sorunumuz var demektir, çünkü uygulamamız gerçekten bozuktur, ya dosyayı bir şekilde eklemeyi unutmuşuzdur (bu durumda oyunumuz çalışmaz) ya da dosyayı eklemişizdir ancak iOS bir nedenden dolayı dosyayı okumamıza izin vermemiştir.

Sebebi ne olursa olsun, bu asla gerçekleşmemesi gereken bir durumdur ve Swift bize fatalError() adında bir fonksiyon sunarak çözülemeyen sorunlara gerçekten net bir şekilde yanıt vermemizi sağlar. fatalError() fonksiyonunu çağırdığımızda -koşulsuz olarak ve her zaman- uygulamamızın çökmesine neden olacaktır.

Kulağa kötü geldiğinin farkındayım, ancak yapmamıza izin verdiği şey önemlidir. Yukarıda bahsettiğimiz durumlar gibi sebeplerle uygulamamızın bozuk bir durumda devam etmesini sağlamaya çalışmanın bir anlamı yoktur. Hemen sonlandırmak ve bize neyin yanlış gittiğine dair net bir açıklama vermek çok iyidir, böylece sorunu düzletebiliriz, işte fatalError() tam olarak bunu yapar.

Şimdi kodumuza göz atalım;

func startGame() {
    // 1. Find the URL for start.txt in our app bundle
    // 1. Uygulama paketimizde start.txt için URL'i bul
    if let startWordsURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
        // 2. Load start.txt into a string
        // 2. start.txt dosyasını bir string'e yükleyin
        if let startWords = try? String(contentsOf: startWordsURL) {
            // 3. Split the string up into an array of strings, splitting on line breaks
            // 3. string'i satır sonuna göre bir string array'e böl
            let allWords = startWords.components(separatedBy: "\n")

            // 4. Pick one random word, or use "silkworm" as a sensible default
            // 4. Rastgele bir kelime seçin veya mantıklı bir varsayılan olarak "silkworm" kullanın
            rootWord = allWords.randomElement() ?? "silkworm"

            // If we are here everything has worked, so we can exit
            // Eğer buradaysak her şey yolunda gitmiştir, bu yüzden çıkabiliriz.
            return
        }
    }

    // If were are *here* then there was a problem – trigger a crash and report the error
    // Eğer *buradaysak* bir sorun var demektir, bir çökme tetikleyip hatayı bildirelim
    fatalError("Could not load start.txt from bundle.")
}

Artık oyun için her şeyi yükleyecek bir methodumuz olduğuna göre, view gösterildiğinde bu methodu gerçekten çağırmamız gerekiyor. SwiftUI, bir view gösterildiğinde bir closure çalıştırmak için bize özel bir modifier verir, böylece startGame() ’i çağırmak ve işleri harekete geçirmek için bunu kullanabiliriz. Aşağıdaki modifier’ı onSubmit() ’ten sonra ekleyin.

.onAppear(perform: startGame)

Eğer oyunu çalıştırırsanız, navigasyon bölümünün üst kısmında rastgele sekiz harfli bir kelime göreceksiniz.

Load word from bundle

Swift UITextChecker ile Doğrulama #

Artık oyunumuz hazır olduğuna göre, bu projenin son kısmı kullanıcının geçersiz kelimeler giremeyeceğinden emin olmaktır. Bunu, her biri tam olarak bir kontrol gerçekleştiren dört küçük method olarak uygulayacağız.

  • Kelime orijinal mi? (daha önce kullanılmış mı?)
  • Kelime mümkün mü? (”silkworm” kelimesinden “car” oluşturulmaya çalışılıyor mu?)
  • Kelime gerçek mi? (İngilizce bir kelime mi gerçekten)

Dikkat ettiyseniz sadece üç method var. 4.method ise hata mesajlarını göstermek olacak.

İlk method ile başlayalım. Bu method tek parametre olarak bir string kabul edecek ve kelimenin daha önce kullanılıp kullanılmadığına bağlı olarak true ya da false döndürecektir. Zaten bir usedWord array’imiz var, bu nedenle kelimeyi contains() methoduna geçebilir ve sonucu şu şekilde geri gönderebiliriz;

func isOriginal(word: String) -> Bool {
    !usedWords.contains(word)
}

Diğer methoda geçelim. Rastgele bir kelimenin başka bir rastgele kelimenin harflerinden oluşup oluşmadığını nasıl kontrol edebiliriz?

Bunun üstesinden gelmenin birkaç yolu vardır, ancak en kolay olanı yapmaya çalışalım. Kök kelimenin değişken bir kopyasını oluşturursak, kullanıcının girdiği kelimenin her bir harfi üzerinde döngü yaparak o harfin kopyamızda olup olmadığını görebiliriz. Eğer varsa, onu kopyadan çıkarırız (böylece iki kez kullanılamaz), sonra devam ederiz. Kullanıcının kelimesinin sonuna başarıyla ulaşırsak, kelime iyidir, aksi takdirde bir hata vardır ve false döndürürüz.

İşte ikinci methodumuz;

func isPossible(word: String) -> Bool {
    var tempWord = rootWord

    for letter in word {
        if let pos = tempWord.firstIndex(of: letter) {
            tempWord.remove(at: pos)
        } else {
            return false
        }
    }

    return true
}

Üçüncü methodumuzda, UIKit’den UITextChecker’ı kullancağız. Swift stringlerini Objective-C stringlerine güvenli bir şekilde köprülemek için, Swift stringlerini UTF-16 sayısını kullanarak bir NSRange instance oluşturmamız gerekir.

Dolayısıyla, son methodumuz, stringleri yanlış yazılmış sözcükler için taramaktan sorumlu olan bir UITextChecker instance oluşturacaktır. Daha sonra string’in tüm uzunluğunu taramak için bir NSRange oluşturacağız, ardından metin denetleyicimizde rangeOfMisspelledWord() methodunu çağıracağız, böylece yanlış kelimeleri arayacak. Bu bittiğinde, yanlış yazılmış kelimenin nerede bulunduğunu bize söyleyen başka bir NSRange geri alacağız, ancak kelime tamamsa, bu aralığın konumu NSNotFound özel değeri olacaktır.

İşte son methodumuz;

func isReal(word: String) -> Bool {
    let checker = UITextChecker()
    let range = NSRange(location: 0, length: word.utf16.count)
    let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")

    return misspelledRange.location == NSNotFound
}

Bu üçünü kullanmadan önce, hata uyarılarını göstermeyi kolaylaştırmak için bazı kodlar ekleyeceğiz. İlk olarak, uyarılarımızı kontrol etmek için bazı property’lere ihtiyacımız var.

@State private var errorTitle = ""
@State private var errorMessage = ""
@State private var showingError = false

Şimdi, aldığı parametrelere göre başlığı ve mesajı ayarlayan, ardından showingError Boolean’ı true değerine çeviren bir method ekleyebiliriz;

func wordError(title: String, message: String) {
    errorTitle = title
    errorMessage = message
    showingError = true
}

Daha sonra .onAppear() ’ın altına bir alert() modifier ekleyerek bunları doğrudan SwiftUI’ye aktarabiliriz;

.alert(errorTitle, isPresented: $showingError) {
    Button("OK") { }
} message: {
    Text(errorMessage)
}

Aslında alert’e herhangi bir Buton eklemezsek, otomatik olarak “OK” yazan bir buton dahil edilir.

Bu sebeple yukarıdaki kodu şu şekilde yazabiliriz;

.alert(errorTitle, isPresented: $showingError) { } message: {
    Text(errorMessage)
}

Şimdi oyunumuzu bitirelim. addNewWord() içindeki // extra validation to come kısmını şu şekilde değiştirelim;

guard isOriginal(word: answer) else {
    wordError(title: "Word used already", message: "Be more original")
    return
}

guard isPossible(word: answer) else {
    wordError(title: "Word not possible", message: "You can't spell that word from '\(rootWord)'!")
    return
}

guard isReal(word: answer) else {
    wordError(title: "Word not recognized", message: "You can't just make them up, you know!")
    return
}

Uygulamayı şimdi çalıştırırsanız, testlerimizden geçmeyen kelimeleri kullanmanıza izin vermeyeceğini göreceksiniz.

Word Scramble App


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 30 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.